package uk.co.richardwkirk.parkrun.geo;

import java.io.IOException;
import java.io.StringReader;

import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import uk.co.richardwkirk.parkrun.data.events.Event;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

public class GeoDataParser {

	String content = null;

	public GeoDataParser(String fileContent) {
		content = fileContent;
	}

	public Region createWorld() throws GeoDataException {
		Document doc = getDomElement(content);
		Region world = createRegion(getFirstElement(doc.getElementsByTagName(("r"))));
		addEvents(world, doc.getElementsByTagName("e"));
		return world;
	}

	
	private Node getFirstElement(NodeList elementsByTagName) {
		return elementsByTagName.item(0);
	}

	private Region createRegion(Node regionElement) throws GeoDataException {
		Region region = null;
		if (regionElement.hasChildNodes()) {
			// Create a non-leaf node and add all elements
			region = new Region();
		}
		else {
			// Create a lead node
			region = new LeafRegion();
		}
		
		// Populate the standard values
		NamedNodeMap attributes = regionElement.getAttributes();
		region.setId(parseIntAttribute(attributes, "id"));
		region.setLocation(new Location(parseDoubleAttribute(attributes, "la"), parseDoubleAttribute(attributes, "lo")));
		region.setName(parseStringAttribute(attributes, "n"));
		region.setUrl(parseStringAttribute(attributes, "u"));
		region.setZoom(parseIntAttribute(attributes, "z"));
		
		// Add the sub regions if there are any
		NodeList subRegionElements = regionElement.getChildNodes();
		for (int i = 0; i < subRegionElements.getLength(); ++i) {
			Node childRegionElement = subRegionElements.item(i);
			if (childRegionElement.getNodeName().equals("r")) {
				region.addRegion(createRegion(childRegionElement));
			}
		}
		
		return region;
	}
	

	private String parseStringAttribute(NamedNodeMap attributes, String name) throws GeoDataException {
		Node attribute = attributes.getNamedItem(name);
		if (attribute != null) {
			return attribute.getTextContent();
		}
		else {
			throw new GeoDataException("Could not find attribute: " + name);
		}
	}

	private double parseDoubleAttribute(NamedNodeMap attributes, String name) throws GeoDataException {
		String text = parseStringAttribute(attributes, name);
		try {
			return Double.parseDouble(text);
		}
		catch (NumberFormatException e) {
			throw new GeoDataException("Could not parse double for attribute: " + name);
		}
	}

	private int parseIntAttribute(NamedNodeMap attributes, String name) throws GeoDataException {
		String text = parseStringAttribute(attributes, name);
		if (text != null) {
			return Integer.parseInt(text);
		}
		else {
			throw new GeoDataException("Could not parse integer for attribute: " + name);
		}
	}

	private void addEvents(Region world, NodeList eventElements) {
		int errorCount = 0;
		for (int i = 0; i < eventElements.getLength(); ++i) {
			try {
				Event event = createEvent(eventElements.item(i));
				Region region = world.getRegion(event.getRegionId());
				region.addEvent(event);
			}
			catch (GeoDataException e) {
				++errorCount;
			}
		}
		world.setErrorCount(errorCount);
	}
	
	private Event createEvent(Node item) throws GeoDataException {
		Event event = new Event();
		
		// Populate the event values
		NamedNodeMap attributes = item.getAttributes();
		event.setId(parseIntAttribute(attributes, "id"));
		event.setShortName(parseStringAttribute(attributes, "n"));
		event.setFullName(parseStringAttribute(attributes, "m"));
		event.setLocation(new Location(parseDoubleAttribute(attributes, "la"), parseDoubleAttribute(attributes, "lo")));
		event.setRegionId(parseIntAttribute(attributes, "r"));
		
		return event;
	}

	public Document getDomElement(String xml) throws GeoDataException {
		Document doc = null;
		DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
		try {
			DocumentBuilder db = dbf.newDocumentBuilder();
			InputSource is = new InputSource();
			is.setCharacterStream(new StringReader(xml));
			doc = db.parse(is);
		} catch (ParserConfigurationException e) {
			throw new GeoDataException("Could not parse GeoData.", e);
		} catch (SAXException e) {
			throw new GeoDataException("Could not parse GeoData.", e);
		} catch (IOException e) {
			throw new GeoDataException("Could not parse GeoData.", e);
		}
		// return DOM
		return doc;
	}

}
